use std::collections::HashMap;

use bon::Builder;
use db::establish_connection;
use dotenvy::dotenv;
use ysbase::func::{short_id, sub_string};
use log::info;
use regex::{Captures, Regex};
use rocket::{launch, routes};
use scraper::{Html, Selector};
use htmd::{Element, HtmlToMarkdown};
use sea_orm::{EntityTrait, Set};
use serde::Serialize;

mod db;
mod web_server;
mod wx;

mod entities;
use entities::{prelude::*, *};
use web_server::{index, check_signature, get_access_token, api_message};



#[launch] // The "main" function of the program
fn rocket() -> _ {
    dotenv().ok();
    log4rs::init_file("log4rs.yaml", Default::default()).unwrap();
    info!("booting up");
    rocket::build().mount("/", routes![index])
        .mount("/mp", routes![check_signature, get_access_token, api_message])
}


#[derive(Builder, Serialize)]
pub struct Author {
    name: String,
    intro: Option<String>,
    avatar: Option<String>,
    url: Option<String>,
}

#[derive(Builder, Serialize)]
pub struct Article {
    title: String,
    content: String,
    author: Option<Author>,
    date: Option<String>,
}

pub async fn insert_article(article: Article) {
    let post_data = post::ActiveModel {
        title: Set(article.title),
        text: Set(article.content),
        ..Default::default()
    };
    let db = establish_connection().await;
    Post::insert(post_data).exec(&db).await.unwrap();
}


fn find_element_attr(element: &Element, attr_name: &str) -> Option<String> {
    for attr in element.attrs.iter() {
        let name = &attr.name.local;
        if name == attr_name {
            return Some(attr.value.to_string())
        }
    }
    None
}

fn is_element_attr_eq(element: &Element, attr_name: &str, attr_value: &str) -> bool {
    for attr in element.attrs.iter() {
        let name = &attr.name.local;
        if name == attr_name {
            return attr.value.to_string() == attr_value
        }
    }
    false
}

fn get_text_by_css(css: &str, fragment: &Html) -> String {
    let selector = Selector::parse(css).unwrap();
    let element = fragment.select(&selector).next().unwrap();
    element.inner_html()
}

fn get_attr_by_css(css: &str, attr: &str, fragment: &Html) -> String {
    let selector = Selector::parse(css).unwrap();
    let element = fragment.select(&selector).next().unwrap();
    element.attr(attr).unwrap().to_string()
}


pub fn zhihu_answer_markdown(html: &str) -> Article {
    let fragment = Html::parse_fragment(&html);

    let author_name = get_text_by_css("div.AuthorInfo-content span.AuthorInfo-name a.UserLink-link", &fragment);
    let author_url = get_attr_by_css("div.AuthorInfo-content span.AuthorInfo-name a.UserLink-link", "href", &fragment);
    let author_avatar = get_attr_by_css("div.AnswerItem-authorInfo span.AuthorInfo-avatarWrapper a.UserLink-link img.AuthorInfo-avatar", "src", &fragment);
    let author_intro = get_text_by_css("div.AuthorInfo-content div.AuthorInfo-detail div.AuthorInfo-badgeText", &fragment);
    let author = Author::builder()
        .name(author_name)
        .url(author_url)
        .avatar(author_avatar)
        .intro(author_intro)
        .build();

    
    let title = get_text_by_css("h1.QuestionHeader-title", &fragment);
    let date_text = get_text_by_css("div.RichContent div.ContentItem-time a span", &fragment);
    let date = sub_string(date_text.trim(), 4, 0);

    let selector = Selector::parse("div.RichContent div.RichContent-inner").unwrap();
    let first = fragment.select(&selector).next().unwrap();
    let html = first.inner_html();

    let converter = HtmlToMarkdown::builder()
        .add_handler(vec!["span"], |e: Element| {
            if is_element_attr_eq(&e, "class", "ztext-math") {
                let tex = find_element_attr(&e, "data-tex");
                if let Some(tex) = tex {
                    return Some(format!("${}$", tex))
                }
            }
            Some(e.content.to_string())
        })
        .skip_tags(vec!["script", "style", "noscript"])
        .build(); 

    let content = converter.convert(&html).unwrap();

    Article::builder().title(title).content(content).author(author).date(date).build()
    
}

pub fn zhihu_article_markdown(html: &str) -> Article {
    let fragment = Html::parse_fragment(&html);

    let author_name = get_text_by_css("div.AuthorInfo-content span.AuthorInfo-name a.UserLink-link", &fragment);
    let author_url = get_attr_by_css("div.AuthorInfo-content span.AuthorInfo-name a.UserLink-link", "href", &fragment);
    let author_avatar = get_attr_by_css("img.AuthorInfo-avatar", "src", &fragment);
    let author_intro = get_text_by_css("div.AuthorInfo-content div.AuthorInfo-detail div.AuthorInfo-badgeText", &fragment);
    let author = Author::builder()
        .name(author_name)
        .url(author_url)
        .avatar(author_avatar)
        .intro(author_intro)
        .build();

    
    let title = get_text_by_css("h1.Post-Title", &fragment);
    let date_text = get_text_by_css("article.Post-Main div.ContentItem-time", &fragment);
    let date = sub_string(date_text.trim(), 4, 0);

    let selector = Selector::parse("div.Post-RichTextContainer div.Post-RichText").unwrap();
    let first = fragment.select(&selector).next().unwrap();
    let html = first.inner_html();

    let converter = HtmlToMarkdown::builder()
        .add_handler(vec!["span"], |e: Element| {
            if is_element_attr_eq(&e, "class", "ztext-math") {
                let tex = find_element_attr(&e, "data-tex");
                if let Some(tex) = tex {
                    return Some(format!("${}$", tex))
                }
            }
            Some(e.content.to_string())
        })
        .skip_tags(vec!["script", "style", "noscript"])
        .build(); 

    let content = converter.convert(&html).unwrap();

    Article::builder().title(title).content(content).author(author).date(date).build()
    
}

pub fn weixin_article_markdown(html: &str) -> Article {
    let fragment = Html::parse_fragment(&html);

    let author_name = format!("{} {}", get_text_by_css("span.rich_media_meta.rich_media_meta_text", &fragment).trim(), get_text_by_css("span#profileBt a", &fragment).trim());
    let author = Author::builder()
        .name(author_name)
        .build();

    
    let title = get_text_by_css("h1.rich_media_title", &fragment);
    let date_text = get_text_by_css("em#publish_time", &fragment);
    let date = date_text.trim().to_owned();

    let selector = Selector::parse("div.rich_media_content").unwrap();
    let first = fragment.select(&selector).next().unwrap();
    let html = first.inner_html();

    let converter = HtmlToMarkdown::builder()
        .add_handler(vec!["span"], |e: Element| {
            if is_element_attr_eq(&e, "class", "ztext-math") {
                let tex = find_element_attr(&e, "data-tex");
                if let Some(tex) = tex {
                    return Some(format!("${}$", tex))
                }
            }
            Some(e.content.to_string())
        })
        .skip_tags(vec!["script", "style", "noscript"])
        .build(); 

    let content = converter.convert(&html).unwrap();

    Article::builder().title(title).content(content).author(author).date(date).build()
    
}

pub fn csdn_article_markdown(html: &str) -> Article {
    let fragment = Html::parse_document(&html);

    let author_name = get_text_by_css("a.follow-nickName", &fragment).trim().to_owned();
    let author_url = get_attr_by_css("a.follow-nickName", "href", &fragment);
    let author_img = get_attr_by_css("div#blogColumnPayAdvert div.column-group-item-one a.item-target img.item-target", "src", &fragment);
    let author = Author::builder()
        .name(author_name)
        .url(author_url)
        .avatar(author_img)
        .build();

    
    let title = get_text_by_css("h1.title-article", &fragment);
    let date = get_attr_by_css("span.blog-postTime", "data-time", &fragment);

    let selector = Selector::parse("div#content_views").unwrap();
    let first = fragment.select(&selector).next().unwrap();
    let html = first.inner_html();

    let converter = HtmlToMarkdown::builder()
        .add_handler(vec!["ul"], |e: Element| {
            if is_element_attr_eq(&e, "class", "pre-numbering") {
                return None
            }
            Some(e.content.to_string())
        })
        .add_handler(vec!["img"], |e: Element| {
            if is_element_attr_eq(&e, "class", "look-more-preCode") {
                return None
            }
            println!("ssss: {}", e.content.to_string());
            Some(e.content.to_string())
        })
        .skip_tags(vec!["script", "style", "noscript"])
        .build();

    let content = converter.convert(&html).unwrap();

    Article::builder().title(title).content(content).author(author).date(date).build()
    
}

pub fn jianshu_article_markdown(html: &str) -> Article {
    let fragment = Html::parse_fragment(&html);

    let author_name = get_text_by_css("div.rEsl9f div._2mYfmT span.FxYr8x a._1OhGeD", &fragment).trim().to_owned();
    let author_url = get_attr_by_css("div.rEsl9f div._2mYfmT a._1qp91i", "href", &fragment);
    let author_img = get_attr_by_css("div.rEsl9f div._2mYfmT a._1qp91i img._13D2Eh", "src", &fragment);
    let author = Author::builder()
        .name(author_name)
        .url(author_url)
        .avatar(author_img)
        .build();

    
    let title = get_text_by_css("h1._1RuRku", &fragment);
    let date = get_attr_by_css("div.s-dsoj time", "datetime", &fragment);

    let selector = Selector::parse("article._2rhmJa").unwrap();
    let first = fragment.select(&selector).next().unwrap();
    let html = first.inner_html();

    let converter = HtmlToMarkdown::builder()
        .add_handler(vec!["ul"], |e: Element| {
            if is_element_attr_eq(&e, "class", "pre-numbering") {
                return None
            }
            Some(e.content.to_string())
        })
        .skip_tags(vec!["script", "style", "noscript"])
        .build(); 

    let content = converter.convert(&html).unwrap();

    Article::builder().title(title).content(content).author(author).date(date).build()
    
}

pub fn replace_download_image(article_markdown: &str) -> String {
    let re = Regex::new(r"!\[(.*?)\]\((.*?)\)").unwrap();
    let mut map = HashMap::new();
    for (_, [_, url]) in re.captures_iter(article_markdown).map(|c| c.extract()) {
        let id = short_id();
        let suffix = url.split(".").last().unwrap();
        let path = format!("./tmp/image_{}.{}", id, suffix);
        map.insert(url.to_string(), path);
    }

    println!("{:?}", map);

    let replaced = re.replace_all(article_markdown, |caps: &Captures| {
        let url = caps.get(1).unwrap().as_str();
        println!("{}", url);
        let path = map.get(url).unwrap();
        format!("![{}]({})", caps.get(0).unwrap().as_str(), path)
    }).to_string();
    replaced
}



#[cfg(test)]
mod tests {
    use dotenvy::dotenv;
    use scraper::{Html, Selector};
    use htmd::{Element, HtmlToMarkdown};
    use serde_json::json;
    use ysbase::func::read_file;

    use crate::{csdn_article_markdown, find_element_attr, insert_article, is_element_attr_eq, jianshu_article_markdown, replace_download_image, weixin_article_markdown, zhihu_answer_markdown, zhihu_article_markdown, Article, Author};

    fn setup() {
        dotenv().ok();
    }

    #[tokio::test]
    async fn test_insert() {
        setup();
        let article = Article::builder()
            .title("test".to_owned()).content("test".to_owned())
            .author(Author::builder().name("test".to_owned()).build()).date("2023-01-01".to_owned()).build();
        insert_article(article).await;
    }

    #[test]
    fn test_replace_download_image() {
        let html = read_file("test_resource/csdn.html");
        let article = csdn_article_markdown(&html);
        let markdown = replace_download_image(&article.content);
        println!("{}", markdown);
    }

    #[tokio::test]
    async fn test_download() {
        let endpoint = "https://pic1.zhimg.com/70/v2-1571f342c1f5e8f955090410678be788_1440w.avis?source=172ae18b&biz_tag=Post";
        let resp = reqwest::get(endpoint).await.unwrap();
        let path = "./tmp/image.jpg";
        if resp.status().is_success() {
            let bytes = resp.bytes().await.unwrap();
            tokio::fs::write(path, &bytes).await.unwrap();
        }
        let is_exists = tokio::fs::try_exists(path).await.unwrap();
        assert!(is_exists);
    }

    #[test]
    fn test_jianshu_article_markdown() {
        let html = read_file("test_resource/jianshu.html");
        let article = jianshu_article_markdown(&html);
        println!("{}", json!(article));
    }

    #[test]
    fn test_csdn_article_markdown() {
        let html = read_file("test_resource/csdn.html");
        let article = csdn_article_markdown(&html);
        println!("{}", json!(article));
    }


    #[test]
    fn test_weixin_article_markdown() {
        let html = read_file("test_resource/weixin.html");
        let article = weixin_article_markdown(&html);
        println!("{}", json!(article));
    }

    #[test]
    fn test_zhihu_article_markdown() {
        let html = read_file("test_resource/zhihu_article.html");
        let article = zhihu_article_markdown(&html);
        println!("{}", json!(article));
    }
    
    #[test]
    fn test_zhihu_markdown() {
        let html = read_file("test_resource/zhihu1.html");
        let article = zhihu_answer_markdown(&html);
        println!("{}", json!(article));
    }

    #[test]
    fn test_add() {
        assert_eq!(1+2, 3);
    }

    #[test]
    fn test_read_file() {
        let path = "test_resource/test.txt";
        let content = read_file(path);
        assert_eq!(content, "Hello, world!");
    }

    #[test]
    fn test_parse_html() {
        let html = r#"
        <ul>
            <li>Foo</li>
            <li>Bar</li>
            <li>Baz</li>
        </ul>
        "#;

        let fragment = Html::parse_fragment(html);
        let selector = Selector::parse("li").unwrap();

        for element in fragment.select(&selector) {
            assert_eq!("li", element.value().name());
        }
    }

    #[test]
    fn test_parse_test_html() {
        let html = read_file("test_resource/test1.html");

        let fragment = Html::parse_fragment(&html);
        let selector = Selector::parse("div.RichContent-inner").unwrap();

        let first = fragment.select(&selector).next().unwrap();
        println!("{}", first.inner_html());
    }

    #[test]
    fn test_parse_make_markdown() {
        let html = read_file("test_resource/test1.html");

        let fragment = Html::parse_fragment(&html);
        let selector = Selector::parse("div.RichContent-inner").unwrap();

        let first = fragment.select(&selector).next().unwrap();

        let html = first.inner_html();

        let converter = HtmlToMarkdown::builder()
            .skip_tags(vec!["script", "style", "noscript"])
            .build();
        println!("{}", converter.convert(&html).unwrap());
    }

    #[test]
    fn test_parse_make_markdown2() {
        let html = read_file("test_resource/test2.html");

        let fragment = Html::parse_fragment(&html);
        let selector = Selector::parse("div.Post-RichTextContainer").unwrap();

        let first = fragment.select(&selector).next().unwrap();

        let html = first.inner_html();
        println!("{}", html);

        let converter = HtmlToMarkdown::builder()
            .add_handler(vec!["span"], |e: Element| {
                if is_element_attr_eq(&e, "class", "ztext-math") {
                    let tex = find_element_attr(&e, "data-tex");
                    if let Some(tex) = tex {
                        return Some(format!("${}$", tex))
                    }
                }
                None
            })
            .skip_tags(vec!["script", "style", "noscript"])
            .build(); 

        println!("{}", converter.convert(&html).unwrap());
    }


}